package ca.uvic.group2.server;

import ca.uvic.group2.ServerFactory;
import ca.uvic.group2.api.*;
import ca.uvic.group2.api.exceptions.*;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import java.text.*;
import java.io.*;
import java.net.*;
import java.util.zip.*;

public class AuditLogImpl extends UnicastRemoteObject implements AuditLog,
                                                                 RemoteServer
{
    private String _serverName = "AuditLog";
    private String _filename;
    private int _port;
    private int transIdSpool = 0;
    private Object transIdLock = new Object();

    private ArrayList logs1 = new ArrayList();
    private ArrayList logs2 = new ArrayList();
    private ArrayList currentLogs = logs1;
    private Object logsLock = new Object();
    private Object writeLock = new Object();
    private LogWriterThread logWriterThread = new LogWriterThread();
    private StringBuffer writeBuffer = new StringBuffer();
    private PrintWriter logWriter;

    public AuditLogImpl() throws RemoteException, CommException, IOException {
        super();
        _filename = getTmpFilename();
        logWriter = new PrintWriter(new BufferedWriter(new FileWriter(_filename, false)));
        logWriterThread.start();
    }

    //
    // RemoteServer interface
    //
    public void serverStart(int port) throws RemoteException, CommException {
        try {
            ServerFactory.serverStart(this, port, RMI_NAME);
            _port = port;
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void serverStop() throws RemoteException, CommException {
        try {
            logWriterThread.terminate();
            logWriterThread.join();
            if (logWriter != null) {
                logWriter.close();
                logWriter = null;
            }
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void setServerName(String name) throws RemoteException {
        _serverName = name;
    }

    public String getServerName()  throws RemoteException {
        return _serverName;
    }

    public String getHostName() throws RemoteException {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException err) {
            return null;
        }
    }

    //
    // AuditLog
    //
    public int getTransId() throws RemoteException {
        synchronized (this.transIdLock) {
            return ++this.transIdSpool;
        }
    }


    public String restartLog() throws RemoteException, IOException {
        System.out.println("Restarting log.....");
        String oldFilename;
        synchronized (this.transIdLock) {
            this.transIdSpool = 0;

            synchronized (this.writeLock) {
                synchronized (this.logsLock) {
                    writeLogs();
                    oldFilename = _filename;
                    _filename = getTmpFilename();
                    System.out.println("Filename: " + _filename);
                    if (logWriter != null) {
                        logWriter.close();
                    }
                    logWriter = new PrintWriter(new BufferedWriter(new FileWriter(_filename, false)));
                }
            }
        }

        return oldFilename;
    }

    private String getTmpDumplogFilename(String header) throws IOException {
        if (header == null) {
            header = "allusers";
        }

        String user = System.getenv("USER");
        File logDir = new File("/tmp/" + user + "/dumplogs");
        if (!logDir.exists() && !logDir.mkdirs()) {
            throw new IOException("Unable to create log folder: " + logDir.getPath());
        }

        return File.createTempFile(header, ".xml", logDir).getPath();
    }

    private String getDumplogFilename(String header) throws IOException {
        if (header == null) {
            header = "allusers";
        }

        String home = System.getenv("HOME");
        File logDir = new File(home + "/462/tmp/logs");
        if (!logDir.exists() && !logDir.mkdirs()) {
            throw new IOException("Unable to create log folder: " + logDir.getPath());
        }

        return File.createTempFile(header, ".xml", logDir).getPath();
    }

    private String getTmpFilename() throws IOException {
        String tmpFolder = System.getProperty("group2.AuditLogFolder", "/tmp");
        System.out.println("Using Audit Log Folder: " + tmpFolder);
        File logDir = new File(tmpFolder);
        if (!logDir.exists() && !logDir.mkdirs()) {
            logDir = new File("/tmp");
        }

        return File.createTempFile("dumplog", "log", logDir).getPath();
    }

    public String dumpLog(String username) throws RemoteException, IOException {
        System.out.println("Dumping log " + _filename);
        String filename;
        synchronized (this.writeLock) {
            synchronized (this.logsLock) {
                writeLogs();
                filename = getTmpDumplogFilename(username);
                SystemLogger xmlLogger = new SystemLogger(filename);

                BufferedReader in = new BufferedReader(new FileReader(_filename));
                LogEntry event = new LogEntry(0);

                String line;
                String userKey = null;
                if (username != null) {
                    userKey = username + ",";
                }

                while ((line = in.readLine()) != null) {
                    try {
                        if (username != null && !line.startsWith(userKey)) {
                            continue;
                        }
                        event.fromString(line);
                        event.toSystemLogger(xmlLogger);
                    } catch (ParseException err) {
                        System.err.println("Dumplog parse failure: " + err.getMessage());
                    }

                }

                xmlLogger.closeLog();
            }
        }

        // Zip the file
        String zipFilename = getDumplogFilename(username) + ".gz";
        GZIPOutputStream zipOut = new GZIPOutputStream(new FileOutputStream(zipFilename));
        FileInputStream zipIn = new FileInputStream(filename);
        byte[] buf = new byte[10240];
        int len;
        while ((len = zipIn.read(buf)) > 0) {
            zipOut.write(buf, 0, len);
        }
        zipIn.close();
        zipOut.finish();
        zipOut.close();

        File rawFile = new File(filename);
        rawFile.delete();

        return zipFilename;
    }

    public void userBuy(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "BUY", username, stockSymbol, null, amount));
        }
    }

    public void userCommitBuy(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "COMMIT_BUY", username, null, null, -1.0f));
        }
    }

    public void userCancelBuy(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "CANCEL_BUY", username, null, null, -1.0f));
        }
    }

    public void userSell(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "SELL", username, stockSymbol, null, amount));
        }
    }

    public void userCommitSell(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "COMMIT_SELL", username, null, null, -1.0f));
        }
    }

    public void userCancelSell(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "CANCEL_SELL", username, null, null, -1.0f));
        }
    }

    public void userAddFunds(String server, int transNum, String username, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "ADD", username, null, null, amount));
        }
    }

    public void userQuote(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "QUOTE", username, stockSymbol, null, -1.0f));
        }
    }

    public void userNewBuyTrigger(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "SET_BUY_AMOUNT", username, stockSymbol, null, amount));
        }
    }

    public void userCommitBuyTrigger(String server, int transNum, String username, String stockSymbol, double price) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "SET_BUY_TRIGGER", username, stockSymbol, null, price));
        }
    }

    public void userCancelBuyTrigger(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "CANCEL_SET_BUY", username, stockSymbol, null, -1.0f));
        }
    }

    public void userNewSellTrigger(String server, int transNum, String username, String stockSymbol, int amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "SET_SELL_AMOUNT", username, stockSymbol, null, amount));
        }
    }

    public void userCommitSellTrigger(String server, int transNum, String username, String stockSymbol, double price) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "SET_SELL_TRIGGER", username, stockSymbol, null, price));
        }
    }

    public void userCancelSellTrigger(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "CANCEL_SET_SELL", username, stockSymbol, null, -1.0f));
        }
    }

    public void userDisplaySummary(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "DISPLAY_SUMMARY", username, null, null, -1.0f));
        }
    }

    public void userDisplayLog(String server, int transNum, String username, String filename) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newUserRequest(server, transNum, "DUMPLOG", username, null, filename, -1.0f));
        }
    }

    //
    // System log
    //

    public void systemBuy(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "BUY", username, stockSymbol, null, amount));
        }
    }

    public void systemCommitBuy(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "COMMIT_BUY", username, null, null, -1.0f));
        }
    }

    public void systemCancelBuy(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "CANCEL_BUY", username, null, null, -1.0f));
        }
    }

    public void systemSell(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "SELL", username, stockSymbol, null, amount));
        }
    }

    public void systemCommitSell(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "COMMIT_SELL", username, null, null, -1.0f));
        }
    }

    public void systemCancelSell(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "CANCEL_SELL", username, null, null, -1.0f));
        }
    }

    public void systemAddFunds(String server, int transNum, String username, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "ADD", username, null, null, amount));
        }
    }

    public void systemQuote(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "QUOTE", username, stockSymbol, null, -1.0f));
        }
    }

    public void systemNewBuyTrigger(String server, int transNum, String username, String stockSymbol, double amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "SET_BUY_AMOUNT", username, stockSymbol, null, amount));
        }
    }

    public void systemCommitBuyTrigger(String server, int transNum, String username, String stockSymbol, double price) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "SET_BUY_TRIGGER", username, stockSymbol, null, price));
        }
    }

    public void systemCancelBuyTrigger(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "CANCEL_SET_BUY", username, stockSymbol, null, -1.0f));
        }
    }

    public void systemNewSellTrigger(String server, int transNum, String username, String stockSymbol, int amount) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "SET_SELL_AMOUNT", username, stockSymbol, null, amount));
        }
    }

    public void systemCommitSellTrigger(String server, int transNum, String username, String stockSymbol, double price) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "SET_SELL_TRIGGER", username, stockSymbol, null, price));
        }
    }

    public void systemCancelSellTrigger(String server, int transNum, String username, String stockSymbol) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "CANCEL_SET_SELL", username, stockSymbol, null, -1.0f));
        }
    }

    public void systemDisplaySummary(String server, int transNum, String username) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "DISPLAY_SUMMARY", username, null, null, -1.0f));
        }
    }

    public void systemDisplayLog(String server, int transNum, String username, String filename) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newSystemRequest(server, transNum, "DUMPLOG", username, null, filename, -1.0f));
        }
    }


    //
    // Account log
    //

    public void accountAdd(String server, int transNum, String username, double funds) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newTransaction(server, transNum, SystemLogger.ACT_ADD, username, funds));
        }
    }

    public void accountRemove(String server, int transNum, String  username, double funds) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newTransaction(server, transNum, SystemLogger.ACT_RMV, username, funds));
        }
    }

    //
    // Quote log
    //

    public void quote(String server, int transNum, double price, String stockSymbol, String username, long qsTimestamp, String key) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newQuoteRequest(server, transNum, price, stockSymbol, username, qsTimestamp, key));
        }
    }

    //
    // Error Log
    //
    public void errorEvent(String server, int transNum, String username, String cmd, String message) throws RemoteException {
        synchronized (this.logsLock) {
            this.currentLogs.add(LogEntry.newErrorEvent(server, transNum, cmd, username, null, null, -1.0f, message));
        }

    }

    private ArrayList rotateLogs() {
        ArrayList previousLogs;
        synchronized (logsLock) {
            previousLogs = currentLogs;
            if (currentLogs == logs1) {
                currentLogs = logs2;
            } else {
                currentLogs = logs1;
            }
        }
        return previousLogs;
    }


    private void writeLogs() {
        synchronized (this.writeLock) {
            ArrayList logs = rotateLogs();
            if (logs.isEmpty()) {
                return;
            }

            Iterator i = logs.iterator();
            while (i.hasNext()) {
                LogEntry e = (LogEntry)i.next();
                writeBuffer.setLength(0);
                e.toString(writeBuffer);
                logWriter.println(writeBuffer.toString());
            }

            if (logWriter != null) {
                logWriter.flush();
            }

            logs.clear();
        }
    }



    //
    // Inner Classes
    //
    class LogWriterThread extends Thread {
        private static final int WRITE_INTERVAL = 5000;
        private static final int CHECK_INTERVAL = 500;

        boolean terminated = false;

        public void run() {
            long lastRun = System.currentTimeMillis();

            while (!terminated) {
                try {
                    Thread.sleep(CHECK_INTERVAL);
                } catch (InterruptedException err) {
                }

                if (System.currentTimeMillis() - lastRun > WRITE_INTERVAL) {
                    lastRun = System.currentTimeMillis();
                    writeLogs();
                }
            }
        }

        public void terminate() {
            this.terminated = true;
        }
    }

    static class LogEntry {
        static final int USER = 0;
        static final int SYSTEM = 1;
        static final int QUOTE = 2;
        static final int DATABASE = 3;
        static final int ERROR = 4;

        int kind;
        String server;
        int transNum;
        String command;
        String username;
        String stockSymbol;
        String filename;
        double amount;
        long timestamp;
        String errorMessage;
        long quoteTimestamp;

        public static LogEntry newErrorEvent(String server,
                                             int transNum,
                                             String command,
                                             String username,
                                             String stockSymbol,
                                             String filename,
                                             double funds,
                                             String errorMessage) {

            LogEntry log = new LogEntry(ERROR);
            log.server = server;
            log.transNum = transNum;
            log.command = command;
            log.username = username;
            log.stockSymbol = stockSymbol;
            log.filename = filename;
            log.amount = funds;
            log.errorMessage = errorMessage;
            return log;
        }

        public static LogEntry newUserRequest(String server,
                                              int transNum,
                                              String command,
                                              String username,
                                              String stockSymbol,
                                              String filename,
                                              double amount) {
            LogEntry log = new LogEntry(USER);
            log.server = server;
            log.transNum = transNum;
            log.command = command;
            log.username = username;
            log.stockSymbol = stockSymbol;
            log.filename = filename;
            log.amount = amount;
            return log;
        }

        public static LogEntry newSystemRequest(String server,
                                                int transNum,
                                                String command,
                                                String username,
                                                String stockSymbol,
                                                String filename,
                                                double amount) {
            LogEntry log = new LogEntry(SYSTEM);
            log.server = server;
            log.transNum = transNum;
            log.command = command;
            log.username = username;
            log.stockSymbol = stockSymbol;
            log.filename = filename;
            log.amount = amount;
            return log;
        }

        public static LogEntry newQuoteRequest(String server,
                                               int transNum,
                                               double amount,
                                               String stockSymbol,
                                               String username,
                                               long qstime,
                                               String cypkey) {
            LogEntry log = new LogEntry(QUOTE);

            log.server = server;
            log.transNum = transNum;
            log.command = cypkey;
            log.username = username;
            log.stockSymbol = stockSymbol;
            log.amount = amount;
            log.quoteTimestamp = qstime;
            return log;
        }

        public static LogEntry newTransaction(String server,
                                              int transNum,
                                              int action,
                                              String username,
                                              double funds) {
            LogEntry log = new LogEntry(DATABASE);
            log.server = server;
            log.transNum = transNum;
            log.command = action == 0 ? "0" : "1";
            log.username = username;
            log.amount = funds;
            return log;
        }

        public LogEntry(int kind) {
            this.kind = kind;
            this.timestamp = System.currentTimeMillis();
        }

        public void toString(StringBuffer buffer) {

            if (username != null) {
                buffer.append(username);
            }
            buffer.append(',');

            buffer.append(Integer.toString(kind));
            buffer.append(',');

            if (server != null) {
                buffer.append(server);
            }
            buffer.append(',');

            buffer.append(Integer.toString(transNum));
            buffer.append(',');

            if (command != null) {
                buffer.append(command);
            }
            buffer.append(',');

            if (stockSymbol != null) {
                buffer.append(stockSymbol);
            }
            buffer.append(',');

            if (filename != null) {
                buffer.append(filename);
            }
            buffer.append(',');

            buffer.append(Double.toString(amount));
            buffer.append(',');

            buffer.append(Long.toString(timestamp));
            buffer.append(',');

            if (errorMessage != null) {
                buffer.append(URLEncoder.encode(errorMessage));
            }
            buffer.append(',');

            buffer.append(Long.toString(quoteTimestamp));
        }

        public void fromString(String str) throws ParseException {
            String[] tokens = str.split(",");
            if (tokens.length != 11) {
                throw new ParseException("Wrong number of tokens", tokens.length);
            }

            int i = 0;
            this.username = tokens[i].length() > 0 ? tokens[i] : null; ++i;
            this.kind = Integer.parseInt(tokens[i]); ++i;
            this.server = tokens[i].length() > 0 ? tokens[i] : null; ++i;
            this.transNum = Integer.parseInt(tokens[i]); ++i;
            this.command = tokens[i].length() > 0 ? tokens[i] : null; ++i;
            this.stockSymbol = tokens[i].length() > 0 ? tokens[i] : null; ++i;
            this.filename = tokens[i].length() > 0 ? tokens[i] : null; ++i;
            this.amount = Double.parseDouble(tokens[i]); ++i;
            this.timestamp = Long.parseLong(tokens[i]); ++i;
            this.errorMessage = tokens[i].length() > 0 ? URLDecoder.decode(tokens[i]) : null; ++i;
            this.quoteTimestamp = Long.parseLong(tokens[i]); ++i;
        }

        public void toSystemLogger(SystemLogger logger) {
            switch (kind) {
            case USER :
                logger.logUserCommand(timestamp, server, transNum, command, username,
                                      stockSymbol, filename, amount);
                break;
            case SYSTEM :
                logger.logSystemEvent(timestamp, server, transNum, command, username,
                                      stockSymbol, filename, amount);
                break;
            case QUOTE :
                logger.logQuoteServer(timestamp, server, transNum, amount, stockSymbol,
                                      username, quoteTimestamp, command);
                break;
            case DATABASE :
                logger.logAccountTransaction(timestamp, server, transNum,
                                             command != null && command.equals("0") ? 0 : 1,
                                             username, amount);
                break;
            case ERROR :
                logger.logErrorEvent(timestamp, server, transNum, command, username,
                                     stockSymbol, filename, amount, errorMessage);
                break;
            }
        }

    }
}
